<?php declare(strict_types=1);
/**
 * @author      xianganyall <xianganyall@gmail.com>
 * @copyright   2023-2025 owner
 **/

namespace Srv\Libs\Plugins\JPush;

use Exception;
use JPush\Client;
use JPush\PushPayload;
use Srv\Apps\Proto\VO\Common\PushMsgVO;
use Srv\Conf\ConfModel\JPushConf;
use Srv\Libs\Boot;
use Srv\Libs\Common\CommJson;
use Srv\Libs\Frame\Logs;

class JPushMsg
{
    private string $appKey                          = '';       // 应用Key
    private string $secret                          = '';       // 秘钥Key
    private string $logPath                         = '';       // 日志路径
    private int $ios                                = 0;        // 苹果推送[0-不推送,1-生产,2-开发,3-生产+开发]
    private int $android                            = 0;        // 安卓推送[0-不推送,1-推送]
    private string $androidUriActivity              = '';       // 安卓通道[华为/小米][com.exp.app.PushClickActivity]
    private string $androidUriAction                = '';       // 安卓通道[OPPO/FCM][com.exp.app.PushClickActivity]
    private ?Client $JPushClient                    = null;     // JPushClient 对象
    private string $userThreadIdPrefix              = 'U_';     // push-threadId-用户
    private string $groupThreadIdPrefix             = 'G_';     // push-threadId-群组
    private string $userTagPrefix                   = 'u';      // 用户标签前缀[必须单个字母]
    private string $userAliaPrefix                  = 'a';      // 用户别名前缀[必须单个字母]
    private string $groupTagPrefix                  = 's';      // 群组标签订阅前缀[必须单个字母]
    private string $groupTagSubPrefix               = 'g';      // 群组标签前缀[必须单个字母]

    /**
     * @param JPushConf $JPushConf
     * __construct
     */
    public function __construct(JPushConf $JPushConf)
    {
        $this->appKey                   = $JPushConf->getAppKey();
        $this->secret                   = $JPushConf->getSecret();
        $this->logPath                  = $JPushConf->getLogPath();
        $this->ios                      = $JPushConf->getIos();
        $this->android                  = $JPushConf->getAndroid();
        $this->androidUriActivity       = $JPushConf->getAndroidUriActivity();
        $this->androidUriAction         = $JPushConf->getAndroidUriAction();
        if(!Boot::isRelease()){     // 开发环境
            $this->userTagPrefix        = 'v';
            $this->userAliaPrefix       = 'b';
            $this->groupTagPrefix       = 't';
            $this->groupTagSubPrefix    = 'h';
        }
        try{
            $this->JPushClient          = new Client($this->appKey, $this->secret, $this->logPath);
        }catch(Exception $Exception){
        }
    }

    /**
     * @param int $userId
     * @return string
     * getThreadIdByUser
     */
    final public function getThreadIdByUser(int $userId):string
    {
        return $this->userThreadIdPrefix.$userId;
    }

    /**
     * @param int $groupId
     * @return string
     * getThreadIdByGroup
     */
    final public function getThreadIdByGroup(int $groupId):string
    {
        return $this->groupThreadIdPrefix.$groupId;
    }

    /**
     * @param int $userId
     * @return string
     * 获取推送用户标签名
     */
    final public function getPushTagNameByUser(int $userId):string
    {
        return $this->userTagPrefix.$userId;
    }

    /**
     * @param int $userId
     * @return string
     * 获取推送用户别名
     */
    final public function getPushAliaNameByUser(int $userId):string
    {
        return $this->userAliaPrefix.$userId;
    }

    /**
     * @param int $groupId
     * @return string
     * 获取推送群组标签名
     */
    final public function getPushTagNameByGroup(int $groupId):string
    {
        return $this->groupTagPrefix.$groupId;
    }

    /**
     * @param int $groupId
     * @return string
     * 获取推送群组标签名
     */
    final public function getPushTagNameByGroupSub(int $groupId):string
    {
        return $this->groupTagSubPrefix.$groupId;
    }

    /**
     * @param PushMsgVO $PushMsgVO
     * @param array $pushSet
     * @param string $pushContent
     * @param string $pushTitle
     * @param array $pushData
     * @param array $pushSetRes
     * @return void
     * genPushMsgData
     */
    private function genPushMsgData(PushMsgVO $PushMsgVO, array $pushSet, string &$pushContent, string &$pushTitle, array &$pushData, array &$pushSetRes):void
    {
        $pushData       = [
            'uId'       => $PushMsgVO->getUId(),
            'type'      => $PushMsgVO->getType(),
            'toUrl'     => $PushMsgVO->getToUrl(),
        ];
        $badgeNum       = $PushMsgVO->getBadgeNum() < 0 ? '+1' : strval($PushMsgVO->getBadgeNum());
        $threadId       = $PushMsgVO->getThreadId();
        $pushTitle      = $PushMsgVO->getTitle();
        $pushContent    = $PushMsgVO->getContent();
        $pushSound      = $PushMsgVO->getSound();
        $pushIcon       = $PushMsgVO->getIcon();
//        $pushTtl        = $PushMsgVO->getTtl() < 1 ? 43200 : $PushMsgVO->getTtl();
//        $pushSilent     = $PushMsgVO->getSilent() === 1 ? 1 : 0;
        $pushSetRes             = ['badge' => $badgeNum];
        // android: title, builder_id, priority, category, style, alert_type, big_text, inbox, big_pic_path, large_icon, intent
        // ios: sound, badge, content-available, mutable-content, category, thread-id
        if(strlen($pushSound) > 0) $pushSetRes['sound'] = $pushSound;
        if(strlen($threadId) > 0) $pushSetRes['thread-id'] = $threadId;
        if(strlen($pushIcon) > 0) $pushSetRes['large_icon'] = $pushIcon;
        $pushSetRes             = count($pushSetRes) > 0 ? array_merge($pushSetRes, $pushSet) : $pushSet;
    }

    /**
     * @param PushMsgVO $PushMsgVO
     * @param array $userIdRecvList
     * @param array $pushSet
     * @return bool
     * 发送用户所有设备推送消息
     */
    public function sendPushUserAllDeviceMsg(PushMsgVO $PushMsgVO, array $userIdRecvList, array $pushSet = []):bool
    {
        if(count($userIdRecvList) < 1) return false;
        $tagUserNameRecvList        = [];
        foreach($userIdRecvList as $userIdRecv){
            $tagUserNameRecvList[]  = $this->getPushTagNameByUser($userIdRecv);
        }
        $tagList                    = array_unique($tagUserNameRecvList);
        if(count($tagList) < 1) return false;
        $pushContent                = '';
        $pushTitle                  = '';
        $pushData                   = [];
        $pushSetRes                 = [];
        $this->genPushMsgData($PushMsgVO, $pushSet, $pushContent, $pushTitle, $pushData, $pushSetRes);
        return $this->sendToTagAliasList([], $tagList, [], [], $pushContent, $pushTitle, $pushData, $pushSetRes);
    }

    /**
     * @param PushMsgVO $PushMsgVO
     * @param array $userIdRecvList
     * @param array $pushSet
     * @return bool
     * 发送用户当前或最后设备推送消息
     */
    public function sendPushUserMsg(PushMsgVO $PushMsgVO, array $userIdRecvList, array $pushSet = []):bool
    {
        if(count($userIdRecvList) < 1) return false;
        $aliasUserNameRecvList      = [];
        foreach($userIdRecvList as $userIdRecv){
            $aliasUserNameRecvList[]= $this->getPushAliaNameByUser($userIdRecv);
        }
        $aliasList                  = array_unique($aliasUserNameRecvList);
        if(count($aliasList) < 1) return false;
        $pushContent                = '';
        $pushTitle                  = '';
        $pushData                   = [];
        $pushSetRes                 = [];
        $this->genPushMsgData($PushMsgVO, $pushSet, $pushContent, $pushTitle, $pushData, $pushSetRes);
        return $this->sendToTagAliasList($aliasList, [], [], [], $pushContent, $pushTitle, $pushData, $pushSetRes);
    }

    /**
     * @param PushMsgVO $PushMsgVO
     * @param int $groupId
     * @param array $userIdRecvList
     * @param array $userIdNotList
     * @param array $pushSet
     * @return bool
     * 发送群组订阅推送消息
     */
    public function sendPushGroupSubMsg(PushMsgVO $PushMsgVO, int $groupId, array $userIdRecvList = [], array $userIdNotList = [], array $pushSet = []):bool
    {
        if($groupId < 1) return false;
        $tagGroupName               = $this->getPushTagNameByGroupSub($groupId);
        $tagUserNameRecvList        = [];
        $tagUserNameNotList         = [];
        if(count($userIdRecvList) > 0) foreach($userIdRecvList as $userIdRecv){
            $tagUserNameRecvList[]  = $this->getPushTagNameByUser($userIdRecv);
        }
        if(count($userIdNotList) > 0) foreach($userIdNotList as $userIdNot){
            $tagUserNameNotList[]   = $this->getPushTagNameByUser($userIdNot);
        }
        $tagList                    = array_unique(array_merge([$tagGroupName], $tagUserNameRecvList));
        if(count($tagList) < 1) return false;
        if(count($tagUserNameNotList) > 0){
            $tagUserNameNotList     = array_unique($tagUserNameNotList);
            $tagList                = array_diff($tagList, $tagUserNameNotList);
        }
        if(count($tagList) < 1) return false;
        $pushContent                = '';
        $pushTitle                  = '';
        $pushData                   = [];
        $pushSetRes                 = [];
        $this->genPushMsgData($PushMsgVO, $pushSet, $pushContent, $pushTitle, $pushData, $pushSetRes);
        return $this->sendToTagAliasList([], $tagList, [], $tagUserNameNotList, $pushContent, $pushTitle, $pushData, $pushSetRes);
    }

    /**
     * @param PushMsgVO $PushMsgVO
     * @param int $groupId
     * @param array $userIdNotList
     * @param array $pushSet
     * @return bool
     * 发送群组推送消息
     */
    public function sendPushGroupMsg(PushMsgVO $PushMsgVO, int $groupId, array $userIdNotList = [], array $pushSet = []):bool
    {
        if($groupId < 1) return false;
        $tagGroupName               = $this->getPushTagNameByGroup($groupId);
        $tagUserNameNotList         = [];
        if(count($userIdNotList) > 0) foreach($userIdNotList as $userIdNot){
            $tagUserNameNotList[]   = $this->getPushTagNameByUser($userIdNot);
        }
        $tagList                    = [$tagGroupName];
        if(count($tagList) < 1) return false;
        if(count($tagUserNameNotList) > 0){
            $tagUserNameNotList     = array_unique($tagUserNameNotList);
            $tagList                = array_diff($tagList, $tagUserNameNotList);
        }
        if(count($tagList) < 1) return false;
        $pushContent                = '';
        $pushTitle                  = '';
        $pushData                   = [];
        $pushSetRes                 = [];
        $this->genPushMsgData($PushMsgVO, $pushSet, $pushContent, $pushTitle, $pushData, $pushSetRes);
        return $this->sendToTagAliasList([], $tagList, [], $tagUserNameNotList, $pushContent, $pushTitle, $pushData, $pushSetRes);
    }

    /**
     * @param string $msgText
     * @param string $title
     * @param array $extData
     * @param array $pushSet
     * @return bool
     * sendToAll
     */
    public function sendToAll(string $msgText, string $title = '', array $extData = [], array $pushSet = []):bool
    {
        try{
            $notificationAndroid    = ['title' => $title, 'extras' => $extData];
            if(strlen($this->androidUriActivity) > 0) $notificationAndroid['uri_activity'] = $this->androidUriActivity;
            if(strlen($this->androidUriAction) > 0) $notificationAndroid['uri_action'] = $this->androidUriAction;
            $notificationIos        = [
                'sound'                 => '',
                'extras'                => $extData,
            ];
            if(count($pushSet) > 0){
                $notificationAndroid    = array_merge($notificationAndroid, $pushSet);
                $notificationIos        = array_merge($notificationIos, $pushSet);
            }
            $JPushClient            = $this->JPushClient->push()
                ->setAudience('all')
                ->setMessage($msgText, $title, 'text', $extData)
//                ->androidNotification($msgText, $this->getNotificationAndroid($notificationAndroid))
                ->iosNotification(strlen($title) > 0 ? ['title' => $title, 'body' => $msgText] : $msgText, $this->getNotificationIos($notificationIos));
            unset($notificationAndroid);
        }catch(Exception $Exception){
            return false;
        }
        return $this->execSend($JPushClient, boolval($pushSet['androidDeny']??0), boolval($pushSet['iOSDeny']??0));
    }

    /**
     * @param array $regIdList
     * @param string $msgText
     * @param string $title
     * @param array $extData
     * @param array $pushSet
     * @return bool
     * sendToRegIdList
     */
    public function sendToRegIdList(array $regIdList, string $msgText, string $title = '', array $extData = [], array $pushSet = []):bool
    {
        try{
            $notificationAndroid    = ['title' => $title, 'extras' => $extData];
            if(strlen($this->androidUriActivity) > 0) $notificationAndroid['uri_activity'] = $this->androidUriActivity;
            if(strlen($this->androidUriAction) > 0) $notificationAndroid['uri_action'] = $this->androidUriAction;
            $notificationIos        = [
                'sound'                 => '',
                'extras'                => $extData,
            ];
            if(count($pushSet) > 0){
                $notificationAndroid    = array_merge($notificationAndroid, $pushSet);
                $notificationIos        = array_merge($notificationIos, $pushSet);
            }
            $JPushClient            = $this->JPushClient->push()
                ->addRegistrationId($regIdList)
                ->setMessage($msgText, $title, 'text', $extData)
//                ->androidNotification($msgText, $this->getNotificationAndroid($notificationAndroid))
                ->iosNotification(strlen($title) > 0 ? ['title' => $title, 'body' => $msgText] : $msgText, $this->getNotificationIos($notificationIos));
            unset($notificationAndroid);
        }catch(Exception $Exception){
            return false;
        }
        return $this->execSend($JPushClient, boolval($pushSet['androidDeny']??0), boolval($pushSet['iOSDeny']??0));
    }

    /**
     * @param array $aliasList
     * @param array $tagList
     * @param array $tagAndList
     * @param array $tagNotList
     * @param string $msgText
     * @param string $title
     * @param array $extData
     * @param array $pushSet
     * @return bool
     * sendToTagAliasList
     */
    public function sendToTagAliasList(array $aliasList, array $tagList, array $tagAndList, array $tagNotList, string $msgText, string $title = '', array $extData = [], array $pushSet = []):bool
    {
        if(count($aliasList) > 1000) $aliasList = array_slice($aliasList, 0, 1000);
        if(count($tagList) > 20) $tagList       = array_slice($tagList, 0, 20);
        if(count($tagAndList) > 20) $tagAndList = array_slice($tagAndList, 0, 20);
        if(count($tagNotList) > 20) $tagNotList = array_slice($tagNotList, 0, 20);
        try{
            $notificationAndroid    = ['title' => $title, 'extras' => $extData];
            if(strlen($this->androidUriActivity) > 0) $notificationAndroid['uri_activity'] = $this->androidUriActivity;
            if(strlen($this->androidUriAction) > 0) $notificationAndroid['uri_action'] = $this->androidUriAction;
            // 字段文档 https://docs.jiguang.cn/jpush/server/push/rest_api_v3_push#ios
            $notificationIos        = [
                'sound'                 => '',      // 字段不存在时会静默
//                'content-available'     => true,    // 是否是唤醒消息[是则通知会自动消失]
                'extras'                => $extData,
            ];
            if(count($pushSet) > 0){
                $notificationAndroid    = array_merge($notificationAndroid, $pushSet);
                $notificationIos        = array_merge($notificationIos, $pushSet);
            }
            $isEmpty                = true;
            $JPushClient            = $this->JPushClient->push()
                ->setMessage($msgText, $title, 'text', $extData)
//                ->androidNotification($msgText, $this->getNotificationAndroid($notificationAndroid))
                ->iosNotification(strlen($title) > 0 ? ['title' => $title, 'body' => $msgText] : $msgText, $this->getNotificationIos($notificationIos));
            if(count($aliasList) > 0){
                $JPushClient->addAlias($aliasList);
                $isEmpty            = false;
            }
            if(count($tagList) > 0){
                $JPushClient->addTag($tagList);
                $isEmpty            = false;
            }
            if(count($tagAndList) > 0){
                $JPushClient->addTagAnd($tagAndList);
                $isEmpty            = false;
            }
            if(count($tagNotList) > 0){
                $JPushClient->addTagNot($tagNotList);
                $isEmpty            = false;
            }
            if($isEmpty) return false;
        }catch(Exception $Exception){
            return false;
        }
        return $this->execSend($JPushClient, boolval($pushSet['androidDeny']??0), boolval($pushSet['iOSDeny']??0));
    }

    /**
     * @param PushPayload $JPushClient
     * @param bool $androidDeny
     * @param bool $iOSDeny
     * @return bool
     * 执行推送
     */
    private function execSend(PushPayload $JPushClient, bool $androidDeny = false, bool $iOSDeny = false):bool
    {
        $result             = true;
        $platformList       = [];
        if(!$androidDeny && $this->android === 1) $platformList[] = 'android';          // 安卓推送[0-不推送,1-推送]
        if(!$iOSDeny && in_array($this->ios, [1, 3], true)) $platformList[] = 'ios';    // 苹果推送[0-不推送,1-生产,2-开发,3-生产+开发]
        if(count($platformList) > 0){
            try{
                $sendResult = $JPushClient->setPlatform($platformList)->setOptions(null, null, null, true)->send();
                if(strlen($this->logPath) > 0) Logs::debug('Android-Send-Succ: '.CommJson::encodeArray($sendResult), __LINE__, __FILE__, $this->logPath);
            }catch(Exception $Exception){
                if(strlen($this->logPath) > 0) Logs::debug('Android-Send-Fail: '.$Exception->getMessage(), __LINE__, __FILE__, $this->logPath);
                $result     = false;
            }
        }
        // 独立推送iOS开发环境
        if(!$iOSDeny && in_array($this->ios, [2, 3], true)){
            try{
                $sendResult = $JPushClient->setPlatform(['ios'])->setOptions(null, null, null, false)->send();
                if(strlen($this->logPath) > 0) Logs::debug('Apple-Send-Succ: '.CommJson::encodeArray($sendResult), __LINE__, __FILE__, $this->logPath);
            }catch(Exception $Exception){
                if(strlen($this->logPath) > 0) Logs::debug('Apple-Send-Fail: '.$Exception->getMessage(), __LINE__, __FILE__, $this->logPath);
                $result     = false;
            }
        }
        return $result;
    }

    /**
     * @param array $notification
     * @return array
     * 检查数据
     */
    private function getNotificationIos(array $notification):array
    {
        if(count($notification) < 1) return [];
        $keyList        = ['sound', 'badge', 'content-available', 'mutable-content', 'category', 'extras', 'thread-id'];
        foreach($notification as $key => $val){
            if(!in_array($key, $keyList, true)){
                $notification[$key] = null;
                unset($notification[$key]);
            }
        }
        return $notification;
    }

    /**
     * @param array $notification
     * @return array
     * 检查数据
     */
    private function getNotificationAndroid(array $notification):array
    {
        if(count($notification) < 1) return [];
        $keyList        = ['title', 'builder_id', 'priority', 'category', 'style', 'alert_type', 'big_text', 'inbox', 'big_pic_path', 'large_icon', 'intent', 'extras', 'uri_activity', 'uri_action'];
        foreach($notification as $key => $val){
            if(!in_array($key, $keyList, true)){
                $notification[$key] = null;
                unset($notification[$key]);
            }
        }
        return $notification;
    }

    /**
     * @param string $regId
     * @return array
     * 获取设备信息
     */
    public function getDevices(string $regId):array
    {
        $resData        = ['result' => 0, 'mobile' => '', 'alias' => '', 'tags' => []];
        try{
            $resArray   = $this->JPushClient->device()->getDevices($regId);
            if(isset($resArray['body']) && is_array($resArray['body'])){
                $resData['result']  = $resArray['http_code'] === 200 ? 1 : 0;
                $resData['mobile']  = $resArray['body']['mobile']??'';
                $resData['alias']   = $resArray['body']['alias']??'';
                $resData['tags']    = $resArray['body']['tags']??[];
            }
        }catch(Exception $Exception){
        }
        return $resData;
    }

    /**
     * @param array $regIdList
     * @return bool
     * clearAliasAndTagsBatch
     */
    public function clearAliasAndTagsBatch(array $regIdList):bool
    {
        if(count($regIdList) < 1) return false;
        foreach ($regIdList as $regId) $this->clearAliasAndTags($regId);
        return true;
    }

    /**
     * @param string $regId
     * @return bool
     * 清除别名和标签
     */
    public function clearAliasAndTags(string $regId):bool
    {
        if(strlen($regId) < 1) return false;
        try{
            $result         = $this->JPushClient->device()->updateDevice($regId, '');
            unset($result);
            $result         = $this->JPushClient->device()->clearTags($regId);
            unset($result);
        }catch(Exception $Exception){
            return false;
        }
        return true;
    }

    /**
     * @param string $regId
     * @param string $alias
     * @param array $tagAddList
     * @param array $tagDelList
     * @param bool $isClear
     * @return bool
     * 设置推送标签
     */
    public function setTags(string $regId, string $alias, array $tagAddList, array $tagDelList, bool $isClear = false):bool
    {
        if(strlen($regId) < 1) return false;
        try{
            if($isClear){
                $result                 = $this->JPushClient->device()->clearTags($regId);
                unset($result);
            }
            if(count($tagDelList) > 0 && count($tagAddList) > 0){
                $tagDelList             = array_diff($tagDelList, $tagAddList);     //不删除本次需要添加的tag
            }
            $tagAddChunkList            = count($tagAddList) < 1 ? [] : array_chunk($tagAddList, 100, false);
            $tagDelChunkList            = count($tagDelList) < 1 ? [] : [array_slice($tagDelList, 0, 100)];     //只处理最多100个删除
            $tagAddChunkListSize        = count($tagAddChunkList);
            $tagDelChunkListSize        = count($tagDelChunkList);
            if($tagAddChunkListSize > 0 || $tagDelChunkListSize > 0){
                if($tagAddChunkListSize > 0){    //ADD
                    foreach($tagAddChunkList as $tagAddValList){
                        if(count($tagAddValList) > 0){
                            $result     = $this->JPushClient->device()->updateDevice($regId, $alias, null, $tagAddValList);
                            unset($result);
                        }
                    }
                }
                if($tagDelChunkListSize > 0){    //DEL
                    foreach($tagDelChunkList as $tagDelValList){
                        if(count($tagDelValList) > 0){
                            $result     = $this->JPushClient->device()->updateDevice($regId, null, null, null, $tagDelValList);
                            unset($result);
                        }
                    }
                }
            }else{
                $result     = $this->JPushClient->device()->updateDevice($regId, $alias);
                unset($result);
            }
        }catch(Exception $Exception){
            return false;
        }
        return true;
    }

    /**
     * @param string $regId
     * @return array
     * 查询推送标签
     */
    public function getTags(string $regId):array
    {
        if(strlen($regId) < 1) return [];
        try{
            $result     = $this->JPushClient->device()->getDevices($regId);
            if(isset($result['body']) && is_array($result['body'])) return $result['body'];
            return is_array($result) ? $result : [];
        }catch(Exception $Exception){
        }
        return [];
    }

    /**
     * @param string $regId
     * @param array $tagList
     * @return bool
     * 添加推送标签
     */
    public function addTags(string $regId, array $tagList):bool
    {
        if(strlen($regId) < 1 || count($tagList) < 1) return false;
        try{
            $tagChunkList   = array_chunk($tagList, 100, false);
            if(count($tagChunkList) > 0) foreach($tagChunkList as $tagValList) if(count($tagValList) > 0) $this->JPushClient->device()->addTags($regId, $tagValList);
        }catch(Exception $Exception){
            return false;
        }
        return true;
    }

    /**
     * @param string $regId
     * @param array $tagList
     * @return bool
     * 删除推送标签
     */
    public function delTags(string $regId, array $tagList):bool
    {
        if(strlen($regId) < 1 || count($tagList) < 1) return false;
        try{
            $tagChunkList   = array_chunk($tagList, 100, false);
            if(count($tagChunkList) > 0) foreach($tagChunkList as $tagValList) if(count($tagValList) > 0) $this->JPushClient->device()->removeTags($regId, $tagValList);
        }catch(Exception $Exception){
            return false;
        }
        return true;
    }
}